#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#


import socket, time

from spinage.net  import iostream
from spinage.help import logging
from spinage.help.protocol import protocol
from spinage.help.codecchain import CodecChain

from response import RC_OK, RC_TIMEOUT


class   Session(object) :
    def __init__(self, address, io_loop, sock=None) :
        self._logger = logging.getLogger(self.__class__.__name__)

        self._address = address
        self._io_loop = io_loop
        self._io_stream = None

        self._codec_chain = CodecChain()
        self._protocol = None

        self._keep_alive = True
        self._auto_connect = True

        self._fd_socket = 0
        self._socket = sock
        if self._socket :
            self._io_stream = iostream.IOStream( self._socket, self._io_loop )
            self._io_stream.set_close_callback( self.__onDisconnect )
            self._fd_socket = self._socket.fileno()

        self._callback = None
        self._request_ongoing = None
        self._ongoing = False

        ## __init__()

    def __del__(self) :
        self.close()
        self._logger.debug('%r died.', self)
        ## __del__()

    def close(self) :
        self.__disconnect( force=True )
        ## close()

    def getAddress(self) :
        return self._address
        ## getAddress()

    def setCodecChain(self, codec_chain) :
        self._codec_chain = codec_chain
        ## setCodecChain()
    
    def setProtocol(self, protocol) :
        self._protocol = protocol

        if not self._io_stream :
            if self._socket :
                self._io_stream = iostream.IOStream( self._socket, self._io_loop )
                self._io_stream.set_close_callback( self.__onDisconnect )

        if self._io_stream :
            self._protocol.bind( self._io_stream )
        ## setProtocol()

    def setAutoConnect(self, auto=True) :
        self._auto_connect = auto
        ## setAutoConnect()

    def closed(self) :
        if self._io_stream :
            return self._io_stream.closed()

        return True
        ## closed()

    ###################################################################

    def cancel(self) :
        if self._ongoing :
            self._ongoing = False
            self._requst_ongoing = None
            # just cancel callback, or callback to caller to triger CANCEL event ???
            self._callback = None

        if self._io_loop.running() :
            self._io_loop.stop()
        if self._io_stream :
            self._io_stream.close()
        self._io_stream = None
        ## cancel()

    ###################################################################

    def sendrecv(self, data, callback) :
        if not self._protocol :
            raise RuntimeError, 'Not protocol is set.'

        self._callback = callback
        self._request_ongoing = data
        self._ongoing = False

        self.__handleRequest()
        ## sendrecv()

    def __handleRequest(self) :
        if self._ongoing and self._io_stream :
            return

        if not self._request_ongoing :
            # why there is nothing but to send
            return

        self._ongoing = True

        ok = self.__connect()
        if not ok :
            return

        # encode request
        request_encoded = self._codec_chain.encode( self._request_ongoing )
        self._protocol.sendrecv( request_encoded, self.__handleResponse )
        ## __handleRequest()

    def __handleResponse(self, status_code, response_encoded, request_encoded) :
        callback = self._callback
        self._callback = None

        request = self._request_ongoing
        self._request_ongoing = None

        self._ongoing = False

        if protocol.PC_OK == status_code :
            status_code = RC_OK
            data_or_error = self._codec_chain.decode( response_encoded )
        else :
            data_or_error = response_encoded

        if callback :
            callback(self, status_code, data_or_error, request)
        ## __handleResponse()

    def __connect(self) :
        if self._io_stream :
            return True

        s = None
        try :
            #self._logger.debug('try to connect to %s ...', self._address)
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
            s.connect( self._address )
            s.setblocking( False )
            self._logger.info('connect to %s (fd:%d) done.', self._address, s.fileno())
        except :
            if s :
                s.close()
            # fail to connect
            self._logger.excpt()

            time_now = time.time()

            if self._auto_connect :
                deadline = time_now + 5.0 
                self._io_loop.add_timeout(deadline, self.__tryToConnect)
            else :
                error = 'fail to connect %s' % str(self._address)
                self._logger.error('%s', error)

                if self._callback :
                    self._callback(self, RC_TIMEOUT, error)
            return False

        self._socket = s
        self._fd_socket = self._socket.fileno()
        self._io_stream = iostream.IOStream( self._socket, self._io_loop )
        self._io_stream.set_close_callback( self.__onDisconnect )

        self._protocol.bind( self._io_stream )
        return True
        ## __connect()

    def __disconnect(self, force=False) :
        if self._keep_alive and not force :
            return

        if self._io_stream :
            if not self._io_stream.closed() : self._io_stream.close()
            self._io_stream = None

        if self._socket :
            try :
                self._socket.close()
            except :
                pass
            self._socket = None
            self._fd_socket = 0
        ## __disconnect()

    def __onDisconnect(self) :
        self._logger.error('connection to %s (fd:%d) broke!', str(self._address), self._fd_socket)
        self._logger.info('%s (fd:%d) disconnected.', str(self._address), self._fd_socket)

        self._io_stream = None
        try :
            self._socket.close()
        except :
            pass
        self._socket = None
        self._fd_socket = 0

        if self._auto_connect :
            deadline = time.time() + 5.0
            self._io_loop.add_timeout(deadline, self.__tryToConnect)
        ## __onDisconnect()

    def __tryToConnect(self) :
        if self._io_stream :
            return

        self.__handleRequest()
        ## __tryToConnect()


    ###################################################################


    def recv(self, callback) :

        if not self._protocol :
            raise RuntimeError, 'Not protocol is set.'

        self._callback = callback
        self._request_ongoing = None
        self._ongoing = False

        self.__do_recv()
        ## recv()

    def __do_recv(self) :
        if self._ongoing :
            return True

        self._ongoing = True

        self._protocol.recv(self.__onIncoming)
        return True
        ## __do_recv()

    def __onIncoming(self, status_code, data, dummy_request=None) :
        callback = self._callback
        self._callback = None

        request = self._request_ongoing
        self._request_ongoing = None

        self._ongoing = False

        if protocol.PC_OK == status_code :
            status_code = RC_OK
            data_or_error = self._codec_chain.decode( data )
        else :
            data_or_error = response_encoded

        callback(self, status_code, data_or_error, request)
        ## __onIncoming()


    ###################################################################


    def send(self, outgoing, callback) :
        if not self._protocol :
            raise RuntimeError, 'Not protocol is set.'

        self._callback = callback
        self._request_ongoing = outgoing
        self._ongoing = False

        self.__do_send()
        ## send()

    def __do_send(self) :
        if self._ongoing :
            return True

        self._ongoing = True

        outgoing_encoded = self._codec_chain.encode( self._request_ongoing )

        self._protocol.send(outgoing_encoded, self.__onOutgoing)
        return True
        ## __do_send()

    def __onOutgoing(self, status_code, data, request=None) :
        callback = self._callback
        self._callback = None

        request = self._request_ongoing
        self._request_ongoing = None

        self._ongoing = False

        if protocol.PC_OK == status_code :
            status_code = RC_OK
            # data should be None
            if data :
                data_or_error = self._codec_chain.decode( data )
            else :
                data_or_error = data
        else :
            data_or_error = response_encoded

        callback(self, status_code, data_or_error, request)
        ## __onIncoming()


    ###################################################################


    ## class Session



#######################################################################


class   SessionPool(object) :
    def __init__(self, io_loop) :
        self._logger = logging.getLogger(self.__class__.__name__)

        self._io_loop = io_loop
        
        self._codec_chain = CodecChain()
        self._codec_by_peer = {}

        self._protocol_factory = None
        self._protocol_factory_by_peer = {}

        # pool : address <-- list of session
        self._pool = {}
        # JUST TEST
        ##self._session_last = None
        ## __init__()

    def __del__(self) :
        for addr,session_list in self._pool.iteritems() :
            for session in session_list :
                session.close()
                del session
        
        for addr in self._pool.keys() :
            del self._pool[ addr ]

        self._pool = None
        ## __del__()

    def acquire(self, address) :
        #self._logger.debug('%s::acquire( address:%s ) ...', self.__class__.__name__, str(address))

        # JUST TEST
        ##if self._session_last :
        ##    session = self._session_last
        ##    self._session_last = None
        ##    return session

        if address in self._pool :
            session_list = self._pool[ address ]
            if len(session_list) > 0 :
                return session_list.pop()

        self._logger.info('new session to %s ...', str(address))
        session = Session( address, self._io_loop )
        session.setCodecChain( self.getCodecChain( address ) )
        session.setProtocol( self.getProtocolFactory( address ).getInstance() )

        return session
        ## acquire()

    def release(self, session) :
        #self._logger.debug('%s::release( address:%s ) ...', self.__class__.__name__, session.getAddress())

        if not session :
            return

        if session.closed() :
            # just discard dead session
            self._logger.error('session %s is closed, and to discard ...', str(session.getAddress()))
            session.close()
            del session
            session = None
        else :
            # JUST TEST
            ##self._session_last = session
            ##return 

            address = session.getAddress()
            if address not in self._pool :
                self._pool[ address ] = [ session, ]
            else :
                self._pool[ address ].append( session )
        ## release()


    ###################################################################

    def getCodecChain(self, peer=None) :
        if peer in self._codec_by_peer :
            return self._codec_by_peer[ peer ]

        return  self._codec_chain
        ## getCodecChain()

    def setCodecChain(self, codec_chain, peer=None) :
        if codec_chain :
            if peer :
                self._codec_by_peer[ peer ] = codec_chain
            else :
                self._codec_chain = codec_chain
        ## setCodecChain()

    def setProtocolFactory(self, protocol_factory, peer=None) :
        if protocol_factory :
            if peer :
                self._protocol_factory_by_peer[ peer ] = protocol_factory
            else :
                self._protocol_factory = protocol_factory
        ## setProtocolFactory()

    def getProtocolFactory(self, peer=None) :
        if peer in self._protocol_factory_by_peer :
            return self._protocol_factory_by_peer[ peer ]

        return self._protocol_factory
        ## getProtocolFactory()


    ## class SessionPool


